package br.com.ava.core.infra.persistencia;

import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.hibernate.CacheMode;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Order;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import br.com.ava.core.ConstantesMensagens;
import br.com.ava.core.infra.ColecaoChecada;
import br.com.ava.core.infra.excecao.NegocioException;
import br.com.ava.core.infra.excecao.ObjetoInexistenteException;

/**
 * Define um DAO no sistema.
 * 
 * @param <T>
 *            Tipo a ser persistido.
 */
@SuppressWarnings("serial")
public abstract class AbstractDAO<T extends ObjetoPersistente> implements
		Serializable {

	private SessionFactory sessionFactory;

	/**
	 * Recupera a classe persistente utilizada.
	 * 
	 * @return Classe persistente utilizada.
	 */
	protected abstract Class<T> getClassePersistente();

	/**
	 * Persiste um objeto no banco de dados.
	 * 
	 * @param obj
	 *            Objeto a ser persistido no baco de dados.
	 */
	@Transactional(isolation = Isolation.DEFAULT, rollbackFor = Exception.class)
	public void salvar(T obj) {
		getCurrentSession().save(obj);
	}

	/**
	 * Altera um objeto no banco de dados.
	 * 
	 * @param obj
	 *            Objeto a ser alterado no banco de dados.
	 */
	@Transactional(isolation = Isolation.DEFAULT, rollbackFor = Exception.class)
	public void alterar(T obj) {
		getCurrentSession().update(obj);
	}

	@Transactional(isolation = Isolation.DEFAULT, rollbackFor = Exception.class)
	public void saveOrUpdate(T obj) {
		getCurrentSession().saveOrUpdate(obj);
	}

	/**
	 * Exclui um objeto do banco de dados.
	 * 
	 * @param obj
	 *            Objeto a ser exclu�do do banco de dados.
	 */
	@Transactional(isolation = Isolation.DEFAULT, rollbackFor = Exception.class)
	public void excluir(T obj) {
		getCurrentSession().delete(obj);
	}

	/**
	 * Verifica se existe objeto persistido no banco de dados com o
	 * identificador informado.
	 * 
	 * @param id
	 *            Identificador do objeto.
	 * @return True caso exista, false caso contr�rio.
	 */
	public boolean existe(Integer id) {
		boolean existe;

		try {
			existe = buscar(id) != null;
		} catch (ObjetoInexistenteException oie) {
			existe = false;
		}

		return existe;
	}

	/**
	 * Recupera o objeto com o identificador informado.
	 * 
	 * @param id
	 *            Identificador do objeto.
	 * @return Objeto com o identificador igual ao informado.
	 * @throws ObjetoInexistenteException
	 *             Caso n�o exista objeto com o identificador informado.
	 */
	@Transactional(isolation = Isolation.DEFAULT, rollbackFor = Exception.class)
	public T buscar(Integer id) throws ObjetoInexistenteException {
		return getClassePersistente().cast(
				getCurrentSession().get(getClassePersistente(), id));
	}

	/**
	 * Recupera o objeto do tipo da classe informada com o identificador
	 * informado.
	 * 
	 * @param id
	 *            Identificador do objeto.
	 * @param classePersistente
	 *            Classe do objeto.
	 * @return Objeto com classe e identificador informados.
	 * @throws ObjetoInexistenteException
	 *             Caso n�o exista objeto com os dados informados.
	 */
	public <D extends ObjetoPersistente> D buscar(Integer id,
			Class<D> classePersistente) throws ObjetoInexistenteException {

		return classePersistente.cast(getCurrentSession().get(
				classePersistente, id));
	}

	/**
	 * Recupera o objeto com o identificador informado sem adicion�-lo na
	 * sess�o.
	 * 
	 * @param <D>
	 *            Tipo do objeto a ser pesquisado pelo id.
	 * @param Identificador
	 *            do objeto.
	 * @param classePersistente
	 *            classe do objeto a ser pesquisado.
	 * @return Objeto com dados informados
	 * @throws ObjetoInexistenteException
	 *             Caso n�o exista objeto com o identificador informado.
	 */
	public <D extends ObjetoPersistente> D buscarDesconectado(Integer id,
			Class<D> classePersistente) throws ObjetoInexistenteException {

		D entidade = classePersistente.cast(getCurrentSession().get(
				classePersistente, id));
		if (entidade != null) {
			desconectar(entidade);
		}

		return entidade;
	}

	public T buscarVerificandoConcorrencia(Integer id) {
		T registro = getClassePersistente().cast(
				getCurrentSession().get(getClassePersistente(), id));

		if (registro == null) {
			throw new NegocioException(
					ConstantesMensagens.MSG_GERAL_ERRO_CONCORRENCIA);
		}

		return registro;
	}

	@Transactional(isolation = Isolation.DEFAULT, rollbackFor = Exception.class)
	public List<T> listar() {
		return ColecaoChecada.checkedList(
				getCurrentSession().createCriteria(getClassePersistente())
						.list(), getClassePersistente());
	}

	protected Criteria createCriteria() {
		return getCurrentSession().createCriteria(getClassePersistente());
	}

	protected Criteria createCriteria(String alias) {
		return getCurrentSession()
				.createCriteria(getClassePersistente(), alias);
	}

	protected Criteria createCriteria(Class<?> classe, String alias) {
		return getCurrentSession().createCriteria(classe, alias);
	}

	protected Criteria createCriteria(CacheMode cacheMode) {
		getCurrentSession().setCacheMode(cacheMode);
		Criteria criteria = createCriteria();
		criteria.setCacheMode(cacheMode);
		return criteria;
	}

	protected Criteria createCriteria(Class<?> classe) {
		return getCurrentSession().createCriteria(classe);
	}

	public <D extends Serializable> void desconectar(D obj) {
		if (getCurrentSession().contains(obj)) {
			getCurrentSession().evict(obj);
		}
	}

	public <D extends Serializable> void desconectar(List<D> objs) {
		if (objs != null && !objs.isEmpty()) {
			for (D obj : objs) {
				getCurrentSession().evict(obj);
			}
		}
	}

	public <D extends Serializable> void reconectar(D obj) {
		getCurrentSession().refresh(obj);
	}

	public void flush() {
		getCurrentSession().flush();
	}

	/**
	 * Adiciona ordena��o � uma criteria.
	 * 
	 * @param propriedade
	 *            Propriedade utilizada na ordena��o.
	 * @param ascendente
	 *            true caso ascendente, false caso descendente.
	 * @param criteria
	 *            Criteria na qual a ordena��o ser� adicionada.
	 */
	protected void adicionarOrdenacao(String propriedade, boolean ascendente,
			Criteria criteria) {
		if (ascendente) {
			criteria.addOrder(Order.asc(propriedade).ignoreCase());
		} else {
			criteria.addOrder(Order.desc(propriedade).ignoreCase());
		}
	}

	/**
	 * Adiciona ordena��o � uma criteria.
	 * 
	 * @param criteria
	 *            Criteria na qual a ordena��o ser� adicionada.
	 * @param propriedades
	 *            Propriedades utilizadas na ordena��o.
	 */
	protected void adicionarOrdenacao(Criteria criteria,
			Map<String, Boolean> propriedades) {

		if (propriedades != null) {
			for (Entry<String, Boolean> entry : propriedades.entrySet()) {
				adicionarOrdenacao(entry.getKey(), entry.getValue(), criteria);
			}
		}
	}

	/**
	 * Recupera a f�brica de sess�es.
	 * 
	 * @return F�brica de sess�es.
	 */
	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}

	/**
	 * Configura a f�brica de sess�es.
	 * 
	 * @param sessionFactory
	 *            F�brica de sess�es.
	 */
	public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	/**
	 * Recupera a sess�o corrente.
	 * 
	 * @return Sess�o correte.
	 */
	public Session getCurrentSession() {
		return sessionFactory.getCurrentSession();
	}

	/**
	 * Limpa as informa��es das tabelas passada como par�metro.
	 * 
	 * @param tables
	 *            tabelas a serem exclu�das.
	 */
	@Transactional(isolation = Isolation.DEFAULT, rollbackFor = Exception.class)
	public void clearTableData(String... tables) {
		for (String table : tables) {
			getCurrentSession().createSQLQuery("delete from " + table)
					.executeUpdate();
			getCurrentSession().flush();
		}
	}
}